package com.liry.dowloadtgz;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import com.alibaba.fastjson.TypeReference;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;

public class DownloadTgz {

    private static final String defaultPackageLockPath = System.getProperty("user.dir");

    private static String tarballs = "tarballs";

    private static final ExecutorService pool = Executors.newFixedThreadPool(10);

    private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();

    private static String NEXUS_HOST = "http://nexus.demo.com";
    // 账户需要有查询和删除权限
    private static String NEXUS_USERNAME = "admin";
    private static String NEXUS_PASSWORD = "admin";
    private static String NEXUS_REPOSITORY = "demo-release";

    private static final String TYPE_DELETE = "delete";

    private static final String TYPE_UPLOAD = "upload";
    private static final String TYPE_DELETE_UPLOAD = "deleteupload";

    public static void main(String[] args) throws IOException, InterruptedException {


        PathMapping pathMapping = resolveArg(args);
        if (StringUtils.isNotBlank(pathMapping.getType())) {
            File startDir = new File(Paths.get(defaultPackageLockPath, tarballs).toString());
            String suffix = ".tgz";
            List<String> filePaths = searchDirectory(startDir, suffix);
            filePaths.forEach(e -> {
                String id = search(NEXUS_HOST, NEXUS_USERNAME, NEXUS_PASSWORD, NEXUS_REPOSITORY, e);
                if (StringUtils.isNotBlank(id)
                        && (TYPE_DELETE.equalsIgnoreCase(pathMapping.getType()) || TYPE_DELETE_UPLOAD.equalsIgnoreCase(pathMapping.getType()))) {
                    delete(NEXUS_HOST, NEXUS_USERNAME, NEXUS_PASSWORD, id);
                }
                if ((StringUtils.isBlank(id) && TYPE_UPLOAD.equalsIgnoreCase(pathMapping.getType()))
                        || TYPE_DELETE_UPLOAD.equalsIgnoreCase(pathMapping.getType())) {
                    upload(NEXUS_HOST, NEXUS_USERNAME, NEXUS_PASSWORD, NEXUS_REPOSITORY, e);
                }
            });
            return;
        }
        System.out.println("下载中...");
        String packageLockPath = pathMapping.getPackageLockPath();
        String rootPath = pathMapping.getRootPath();

        Map<String, String> depMap = loadDepForLockFile(packageLockPath);
        if (depMap == null) {
            return;
        }

        CountDownLatch latch = new CountDownLatch(depMap.size());
        long start = System.currentTimeMillis();
        System.out.println("下载目录：" + rootPath);

        AtomicInteger success = downloadLockFiles(rootPath, depMap, latch);
        downloadProgress(depMap.size(), latch);
        latch.await();

        System.out.println();
        System.out.println("-----------------------------------");
        System.out.println(String.format("总tgz个数：%d，完成下载：%d", depMap.size(), success.get()));
        long u = (System.currentTimeMillis() - start) / 1000;
        long m = u / 60;
        System.out.println(String.format("耗时：%d 秒，%d 分钟", u, m));
    }

    public static List<String> searchDirectory(File dir, String suffix) {
        List<String> filePath = new ArrayList<>();
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    filePath.addAll(searchDirectory(file, suffix)); // 递归子目录
                } else {
                    if (file.getName().endsWith(suffix)) {
                        filePath.add(file.getAbsolutePath()); // 打印文件路径
                    }
                }
            }
        }
        return filePath;
    }

    /**
     * 下载文件执行入口
     *
     * @param rootPath 文件下载的根目录
     * @param depMap   依赖信息（key：url，value:name）
     * @param latch    锁
     * @return 成功下载个数
     */
    private static AtomicInteger downloadLockFiles(String rootPath, Map<String, String> depMap, CountDownLatch latch) {
        AtomicInteger success = new AtomicInteger(0);
        for (Map.Entry<String, String> entry : depMap.entrySet()) {
            String depName = entry.getValue();
            String resolved = entry.getKey();
            depName = depName.replace("node_modules/", "");
            String finalDepName = depName;
            pool.execute(() -> {
                try {
                    downloadFile(rootPath, finalDepName, resolved);
                } catch (IOException e) {
                    System.out.println(String.format("%s 下载失败！", resolved));
                } finally {
                    latch.countDown();
                }
                success.incrementAndGet();
            });
        }
        return success;
    }

    /**
     * 解析参数
     *
     * @param args 参数信息
     * @return 参数对象
     */
    private static PathMapping resolveArg(String[] args) {
        PathMapping result = new PathMapping();
        result.setPackageLockPath(defaultPackageLockPath);
        result.setRootPath(Paths.get(result.getPackageLockPath(), tarballs).toString());
        if (args != null && args.length != 0) {
            Map<String, String> tempMap = new HashMap<>();
            for (String arg : args) {
                String[] split = arg.split("=");
                if (split.length != 2) {
                    continue;
                }
                tempMap.put(split[0], split[1]);
            }
            String tempRoot = tempMap.get("rootPath");
            String tempLock = tempMap.get("lockPath");
            String type = tempMap.get("type");
            NEXUS_HOST = tempMap.get("h");
            NEXUS_USERNAME = tempMap.get("u");
            NEXUS_PASSWORD = tempMap.get("p");
            NEXUS_REPOSITORY = tempMap.get("r");
            result.setType(type);
            String folder = tempMap.get("folder");
            if (StringUtils.isNotBlank(folder)) {
                tarballs = folder;
            }
            if (StringUtils.isNotBlank(tempRoot)) {
                result.setRootPath(Paths.get(tempRoot, tarballs).toString());
            }
            if (StringUtils.isNotBlank(tempLock)) {
                result.setPackageLockPath(tempLock);
            }
        }
        return result;
    }

    /**
     * 从lock文件中加载依赖
     *
     * @param packageLockPath lock路径
     * @return key: url, value：name
     */
    private static Map<String, String> loadDepForLockFile(String packageLockPath) throws IOException {
        File lockFile = new File(packageLockPath);
        Map<String, String> depMap = new HashMap<>();
        if (lockFile.isDirectory()) {
            String[] lockFiles = lockFile.list();
            if (lockFiles == null) {
                System.out.println("没有找到可以读取的lock文件");
                return null;
            }
            System.out.println("lock目录：" + packageLockPath);
            List<String> tempLockFileNames = new ArrayList<>();
            for (String file : lockFiles) {
                if (!file.contains("lock")) {
                    continue;
                }
                String tempLockFilePath = Paths.get(packageLockPath, file).toString();
                Map<String, String> temMap = readLock(tempLockFilePath);
                depMap.putAll(temMap);
                tempLockFileNames.add(tempLockFilePath);
            }
            System.out.println("lock文件：" + String.join(",", tempLockFileNames));
        } else {
            System.out.println("lock文件：" + packageLockPath);
            Map<String, String> temMap = readLock(packageLockPath);
            depMap.putAll(temMap);
        }
        return depMap;
    }

    /**
     * 下载进度
     * @param total 总依赖个数
     * @param latch 锁
     */
    public static void downloadProgress(int total, CountDownLatch latch) {
        if (latch.getCount() == 0) {
            System.out.print("\b\b\b\b\b\b100%");
            return;
        }
        System.out.print("进度：00.00%");
        new Thread(() -> {
            while (latch.getCount() != 0) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                BigDecimal s = new BigDecimal(total - latch.getCount());
                BigDecimal f = new BigDecimal(total);
                s = s.divide(f, 4, RoundingMode.HALF_DOWN);
                DecimalFormat format = new DecimalFormat("00.00%");
                System.out.print("\b\b\b\b\b\b" + format.format(s));
            }
        }).start();
    }

    /**
     * @return key: url, value：name
     */
    public static Map<String, String> readLock(String lockFile) throws IOException {
        if (lockFile.endsWith("yarn.lock")) {
            return readYarnLockMap(lockFile);
        } else {
            return readPackageLockMap(lockFile);
        }
    }

    /**
     * 从yarn.lock读取依赖
     * @param lockFile yarn.lock文件
     * @return key:url, value:name
     */
    public static Map<String, String> readYarnLockMap(String lockFile) throws IOException {
        Map<String, String> depMap = new HashMap<>();
        String filePath = "";
        String tempStr = null;
        try (BufferedReader br = new BufferedReader(new FileReader(lockFile));) {
            while ((tempStr = br.readLine()) != null) {
                if (tempStr.endsWith(":") && !tempStr.contains("pendencies")) {
                    filePath = getYarnlockDepName(tempStr);
                }
                if (!tempStr.contains("resolved")) {
                    continue;
                }
                String resolved = getYarnLockResolved(tempStr);
                depMap.put(resolved, filePath);

            }
        }
        return depMap;
    }

    /**
     * 获取依赖的url，只适用于yarn.lock
     * @param tempStr 文本信息
     * @return url
     */
    private static String getYarnLockResolved(String tempStr) {
        String[] split = tempStr.trim().split(" ");
        String resolved = split[1].trim();
        resolved = StringUtils.removeStart(resolved, "\"");
        resolved = StringUtils.removeEnd(resolved, "\"");
        if (resolved.contains("#")) {
            resolved = resolved.substring(0, resolved.indexOf("#"));
        }
        return resolved;
    }

    /**
     * 获取依赖的名称，只适用于yarn.lock
     * @param tempStr 文本信息
     * @return name
     */
    public static String getYarnlockDepName(String tempStr) {
        String filePath = "";
        String[] split = tempStr.split(",");
        String s = split[0];
        s = StringUtils.removeStart(s, "\"");
        filePath = s.substring(0, s.lastIndexOf("@"));
        if (filePath.contains(":")) {
            filePath = filePath.substring(filePath.indexOf(":") + 1);
        }
        return filePath;
    }

    /**
     * 读取package-lock依赖
     * @param lockFile package-lock文件路径
     * @return key:url, value:name
     */
    public static Map<String, String> readPackageLockMap(String lockFile) throws IOException {
        Map<String, String> depMap = new HashMap<>();
        String tempStr = null;
        StringBuilder jsonStr = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new FileReader(lockFile));) {
            while ((tempStr = br.readLine()) != null) {
                jsonStr.append(tempStr);
            }
        }
        JSONObject json = JSON.parseObject(jsonStr.toString());
        if (json == null) {
            System.out.println("没有读取到数据");
            return depMap;
        }
        String dependenciesKey = "dependencies";
        JSONObject dependencies = json.getJSONObject(dependenciesKey);
        // 兼容高版本npm
        if (dependencies == null) {
            dependenciesKey = "packages";
            dependencies = json.getJSONObject(dependenciesKey);
        }
        depMap = getResolvedMap(dependencies, dependenciesKey);
        return depMap;
    }

    private static Map<String, String> getResolvedMap(JSONObject json, String key) {
        Map<String, String> depMap = new HashMap<>();
        if (json != null && !json.isEmpty()) {
            for (Map.Entry<String, Object> entry : json.entrySet()) {
                String depName = entry.getKey();
                if (StringUtils.isBlank(depName)) {
                    continue;
                }
                JSONObject value = (JSONObject) entry.getValue();
                String resolved = value.getString("resolved");
                depMap.put(resolved, depName);
                JSONObject dependencies = value.getJSONObject(key);
                if (dependencies != null && !dependencies.isEmpty()) {
                    depMap.putAll(getResolvedMap(dependencies, key));
                }
            }
        }
        return depMap;
    }

    /**
     * 下载文件
     * 下载的路径为 rootPath / depName
     * @param rootPath 下载的根目录
     * @param depName 依赖名称
     * @param resolved 依赖的地址
     */
    public static void downloadFile(String rootPath, String depName, String resolved) throws IOException {
        HttpURLConnection urlConnection = getHttpURLConnection(resolved);
        int responseCode = urlConnection.getResponseCode();
        if (responseCode != 200) {
            throw new RuntimeException("下载失败");
        }

        InputStream is = urlConnection.getInputStream();
        byte[] data = new byte[1024];

        File path = new File(Paths.get(rootPath, depName).toString());
        if (!path.exists()) {
            path.mkdirs();
        }
        String fileName = parseFileName(resolved);
        File tgzFile = new File(Paths.get(path.getAbsolutePath(), fileName).toString());
        if (tgzFile.exists()) {
            tgzFile.delete();
        }
        try (OutputStream os = new FileOutputStream(tgzFile, true)) {
            int l = 0;
            while (((l = is.read(data)) != -1)) {
                os.write(data, 0, l);
                os.flush();
            }
        }
        is.close();
    }

    private static String parseFileName(String resolved) {
        String fileName = resolved.substring(resolved.lastIndexOf("/") + 1);
        if (fileName.contains("tgz?")) {
            fileName = fileName.substring(0, fileName.indexOf("tgz?")) + "tgz";
        }
        return fileName;
    }

    public static HttpURLConnection getHttpURLConnection(String resolved) throws IOException {
        URL url = new URL(resolved);
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("GET");
        urlConnection.setDoOutput(true);
        urlConnection.setDoInput(true);
        urlConnection.setReadTimeout(40000);
        urlConnection.setRequestProperty("Connection", "Keep-Alive");
        urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        urlConnection.connect();
        return urlConnection;
    }

    private static String search(String host, String username, String password, String repository, String path) {
        path = path.replace("\\", "/");
        path = path.replace("/-/", "/");
        path = path.replace(defaultPackageLockPath.replace("\\", "/") + "/" + tarballs.replace("\\", "/") + "/", "");
        String fileName = path.substring(path.lastIndexOf('/') + 1);
        String filePath = path.substring(0, path.lastIndexOf('/') );
        String artifa = filePath.substring(filePath.lastIndexOf('/') + 1);
        int start = artifa.length() + 1;
        int end = fileName.lastIndexOf('.');
        String version = fileName.substring(start, end);
        String url = String.format("%s/service/rest/v1/search/assets?repository=%s&version=%s&name=%s", host, repository, version, artifa);
        Request request = new Request.Builder()
                .addHeader("Authorization", getToken(username, password))
                .addHeader("Connection", "keep-alive")
                .url(url)
                .get()
                .build();
        Call call = HTTP_CLIENT.newCall(request);
        try (Response response = call.execute()) {
            if (response.code() == 200 && response.body() != null) {
                JSONObject jsonObject = JSONObject.parseObject(response.body().string());
                List<Asset> result = jsonObject.getObject("items", new TypeReference<List<Asset>>() {
                });
                if (result == null || result.size() == 0) {
                    return "";
                } else {
                    for (int i = 0; i< result.size(); i++) {
                        Asset asset = result.get(i);
                        String assetPath = asset.getPath();
                        System.out.println("assetPath:" + assetPath);
                        assetPath = assetPath.replace("/-/", "/");
                        System.out.println("assetPath:" + assetPath);
                        System.out.println("path:" + path);
                        if (assetPath.equals(path)) {
                            return asset.getId();
                        }
                    }
                }
            } else {
                System.out.println("组件搜索出错，http响应：" + response.body());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void delete(String host, String username, String password, String id) {
        System.out.println("执行删除: " + id);
        String url = String.format("%s/service/rest/v1/assets/%s", host, id);
        Request request = new Request.Builder()
                .addHeader("Authorization", getToken(username, password))
                .addHeader("Connection", "keep-alive")
                .url(url)
                .delete()
                .build();
        Call call = HTTP_CLIENT.newCall(request);
        try (Response response = call.execute()) {
            if (response.code() == 204) {
                System.out.println("删除成功！");
            } else {
                System.out.println("删除失败! 状态码：" + response.code());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void upload(String host, String username, String password, String reposi, String path) {
        System.out.println("执行上传: " + path);
        String url = String.format("%s/service/rest/v1/components?repository=%s", host, reposi);
        File file = new File(path);
        RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM);
        builder.addFormDataPart("r.asset", file.getName(), fileBody);
        MultipartBody body = builder.build();
        Request request = new Request.Builder()
                .addHeader("Authorization", getToken(username, password))
                .addHeader("Connection", "keep-alive")
                .url(url)
                .post(body)
                .build();
        Call call = HTTP_CLIENT.newCall(request);
        try (Response response = call.execute()) {
            if (response.code() == 204) {
                System.out.println("上传成功！");
            } else {
                System.out.println("上传失败! 状态码：" + response.code());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String getToken(String name, String password) {
        String authString = name + ":" + password;
        byte[] authEncBytes = Base64.getEncoder().encode(authString.getBytes(StandardCharsets.UTF_8));
        return "Basic " + new String(authEncBytes);
    }

}
